using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace Rockfan.Framework.Audio.Midi
{	
	internal sealed class MidiFormat
	{
		private const string MIDI_HEADER = "MThd";
		private const string MIDI_TRACK = "MTrk";
	
		#region Properties
	
		/// <summary>
		/// Gets a midi track object at the supplied index
		/// </summary>
		public MidiTrack this[int index]
		{
			get { return Tracks[index]; }
		}
	
		/// <summary>
		/// Gets a midi track object with the name givin, returns null
		/// if track name does not exist
		/// </summary>
		public MidiTrack this[string trackName]
		{
			get			
			{
				foreach(var track in Tracks)
				{
					if (track.Name == trackName)
						return track;
				}
				
				return null;
			}
		}
		
		/// <summary>
		/// Gets the Amount of tracks this object contains
		/// </summary>
		public int Count
		{
			get { return Tracks.Count; }
		}
		
		/// <summary>
		/// Gets a value indicating the pulses per quarter notes
		/// </summary>
		public int PPQN { get; private set;}
		
		/// <summary>
		/// Gets a value indicating the type of file this object represents
		/// </summary>
		public FileType FileType { get; private set; }

        public MidiTrack TempoTrack { get; private set; }

        public MidiTrack TimeSignatureTrack { get; private set; }
	
		#endregion
	
		/// <summary>
		///	Constructs a new instance of this object
		/// </summary>
		private MidiFormat(int ppqn)
		{ 
			this.PPQN = ppqn;

            TempoTrack = new MidiTrack(480);			
			Tracks = new List<MidiTrack>();
			TimeSignatureTrack = new MidiTrack(480);
		}
		
		public int GetOffset(float delta)
		{
			var offset = 0;
			var totalTime = 0.0f;			
			var previousOffset = 0;
			var lastTempo = TempoTrack.Events[0] as TempoEvent;
			
			for (int i = 1; i < TempoTrack.Events.Count; i++)
			{
				var tEvent = (TempoEvent)TempoTrack.Events[i];
				var length = tEvent.Start - previousOffset;
                var lengthInSeconds = ConvertToSeconds(length, lastTempo.Tempo);
				
				if ((totalTime + lengthInSeconds) > delta)
					break;
					
				totalTime += lengthInSeconds;
				previousOffset = tEvent.Start;
				lastTempo = tEvent;
			}
			
			offset = ConvertToOffset(delta - totalTime, lastTempo.Tempo);
			return previousOffset + offset;
		}

        public float GetSeconds(int delta)
        {
            var offset = 0.0f;
            var totalTime = 0.0f;
            var previousOffset = 0;
            var lastTempo = TempoTrack.Events[0] as TempoEvent;

            for (int i = 1; i < TempoTrack.Events.Count; i++)
            {
                var tEvent = (TempoEvent)TempoTrack.Events[i];
                var length = tEvent.Start - previousOffset;
                var lengthInSeconds = ConvertToSeconds(length, lastTempo.Tempo);

                if (tEvent.Start > delta)
                    break;

                totalTime += lengthInSeconds;
                previousOffset = tEvent.Start;
                lastTempo = tEvent;
            }

            if (lastTempo != null)
                offset = ConvertToSeconds(delta - lastTempo.Start, lastTempo.Tempo);

            return totalTime + offset;
        }
		
		public int ConvertToOffset(float delta, int tempo)
		{
            return (int)((delta * PPQN * 1000000.0f) / (float)(tempo * 1000));
		}
		
		public float ConvertToSeconds(int offset, float tempo)
		{
            return (offset * tempo * 1000) / (PPQN * 1000000.0f);
		}
		
		public static MidiFormat Load(string fileLocation)
		{
			if (!File.Exists(fileLocation))
				return null;
				
			var midi = default(MidiFormat);
				
			using (var fStream = new FileStream(fileLocation, FileMode.Open))
				midi = Load(fStream);
				
			return midi;
		}
		
		public static MidiFormat Load(Stream stream)
		{
			if (stream == null)
				return null;
				
			var midi = default(MidiFormat);
			stream.Seek(0, SeekOrigin.Begin);
			
			using (var reader = new BinaryReader(stream))
			{				
				VerifyReaderValue(reader, MIDI_HEADER);
				reader.ReadUInt32(); // ignore
				reader.ReadUInt16(); // ignore
				
				var trackCount = ReverseEndian(reader.ReadUInt16());
				var ppqn = ReverseEndian(reader.ReadUInt16());
				midi = new MidiFormat(ppqn);
				
				#region Read All Tracks
								
				for (int i = 0; i < trackCount; i++)
				{
					VerifyReaderValue(reader, MIDI_TRACK);
				
					var trackLength = ReverseEndian(reader.ReadUInt32());
					var events = new List<BaseMidiEvent>();
					var track = new MidiTrack(ppqn);
					midi.Tracks.Add(track);
					
					var start = 0;
					var runningStatus = (byte)0;
					var position = reader.BaseStream.Position;
					
					#region Read All Track info
					
					while(reader.BaseStream.Position < (position + trackLength))
					{
						var midiEvent = default(BaseMidiEvent);
						var delta =  (int)ReadVariableLengthNumber(reader);
						var status = reader.ReadByte();
						
						start += delta;

                        if (status != 0xff)
                        {
                            byte readData;
                            if ((status & 0x80) == 0x80)
                            {
                                readData = reader.ReadByte();
                                runningStatus = status;
                            }
                            else
                            {
                                readData = status;
                                status = runningStatus;
                            }
                            byte command = (byte)(status >> 4);

                            var pulses = reader.ReadByte();

                            switch (command)
                            {
                                case 8: // Note off
                                    midiEvent = new NoteEvent(start, readData, pulses, false);
                                    break;
                                case 9: // Note On
                                    var noteOn = (command == 9 && pulses != 0);
                                    midiEvent = new NoteEvent(start, readData, pulses, noteOn);
                                    break;

                                case 0x0b: // Ignore value
                                case 0x0c: // Ignore value
                                case 0x0e: // Ignore value
                                    break;

                                default:
                                    throw new ArgumentException(string.Format("Unknown command {0} at {1}", command.ToString("X"), reader.BaseStream.Position));
                            }
                        }
                        else
                        {
                            var command = reader.ReadByte();
                            var length = ReadVariableLengthNumber(reader);
                            var data = reader.ReadBytes((int)length);

                            switch (command)
                            {
                                case 1:

                                    midiEvent = new TextEvent(start, Encoding.ASCII.GetString(data));
                                    break;

                                case 3: // TrackName

                                    var name = Encoding.ASCII.GetString(data);

                                    if (!string.IsNullOrEmpty(name))
                                        track.Name = name.ToUpper(); // Make the track easy to search
                                    break;

                                case 0x2f: // this is the end of track command
                                    break;

                                case 0x51:

                                    if (length != 3)
                                        throw new ArgumentException(string.Format("type: {0} expects a length of 3 at {1}", command.ToString("X"), start));

                                    var tempo = data[0] << 0x10;
                                    tempo |= data[1] << 0x08;
                                    tempo |= data[2];

                                    midi.TempoTrack.Events.Add(new TempoEvent(start, tempo));
                                    break;

                                case 0x58:
                                    if (length != 4)
                                        throw new ArgumentException(string.Format("type: {0} expects a length of 4 at {1}", command.ToString("X"), start));

                                    midi.TimeSignatureTrack.Events.Add(new TimeSignatureEvent(start, data[0], data[1], data[2], data[3]));
                                    break;
                            }
                        }
						
						if (midiEvent != null)
							events.Add(midiEvent);
                    }

                    #endregion

                    track.Events.AddRange(events);
					track.Events.Sort(EventComparer);
					reader.BaseStream.Seek(position + trackLength, SeekOrigin.Begin);					
				}
				
				// Set the FileType for this newly created midi object.
				// is GH1 if "t1 gems" trackname is present
				// is GH4 if "drums / vocals" trackname is present
				// is GH5Plus, if forced hopo's are visible	
				
				#endregion
			}
			
			return midi;
		}
						
		private static uint ReverseEndian(uint value)
		{
            return ((((value >> 0x18) | (value << 0x18)) | ((value >> 8) & 0xff00)) | ((value << 8) & 0xff0000));
		}
		
		private static ushort ReverseEndian(ushort value)
		{
            return (ushort)(((value >> 8) | (value << 8)) & 0xffff);
		}

		private static long ReadVariableLengthNumber(BinaryReader reader)
		{
			var value = 0L;
			var data = default(byte);
			
			do
			{
				data = reader.ReadByte();
                value = (value << 7) + (data & 0x7f);
			}
            while ((data & 0x80) != 0);
			
			return value;
		}

		private static int EventComparer(BaseMidiEvent left, BaseMidiEvent right)
		{
			// Sort all of the NoteEvents, so that all the of the NoteBegin events happen before NoteEnd for the same note type
				
			#region Old Method
			
			if (left is NoteEvent && right is NoteEvent && left.Start == right.Start)
			{
				var note1 = (NoteEvent)left;
				var note2 = (NoteEvent)right;

                if (note1.IsNoteOn && !note2.IsNoteOn)
                    return 1;

                else if (!note1.IsNoteOn && note2.IsNoteOn)
                    return -1;
					
				return 0;
			}
			
			#endregion
						
			return left.Start - right.Start;
		}
		
		private static void VerifyReaderValue(BinaryReader reader, string value)
		{
			var position = reader.BaseStream.Position;
			var readerValue = Encoding.ASCII.GetString(reader.ReadBytes(value.Length));

            if (readerValue != value)
				throw new ArgumentException(string.Format("{0} does not match {1}", readerValue, value));				
		}

		private List<MidiTrack> Tracks;
	}	
}